Skip to content

test(objectql): pin listDrafts header-only 投影 —— #6599 所述字段级泄露经实测不存在 - #7014

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-6599-drafts-authoring-gate
Aug 9, 2026
Merged

test(objectql): pin listDrafts header-only 投影 —— #6599 所述字段级泄露经实测不存在#7014
os-project-manager merged 2 commits into
mainfrom
claude/issue-6599-drafts-authoring-gate

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Refs #6599

本 PR 不修复 #6599 所述缺陷 —— 因为该缺陷经实测不存在

#6599 认为 GET /api/v1/meta/_draftsGET /metadata/_drafts 会把 pending object draft 的完整 fields 映射吐给任何已认证调用方(label、type、picklist options、formula、requiredPermissions)。实测不成立。

SysMetadataRepository.listDraftspackages/metadata-protocol/src/sys-metadata-repository.ts:848)是一个显式六键投影,行上存 schema 的 body 列从头到尾没有被读过:

return (rows as any[]).map((row) => ({
  type: row.type,
  name: row.name,
  organizationId: row.organization_id ?? null,
  packageId: row.package_id ?? null,
  updatedAt: row.updated_at ?? row.created_at ?? null,
  updatedBy: row.updated_by ?? row.created_by ?? null,
}));

先写预测、后跑探针(喂进一行 body 载有 salary_grade + requiredPermissions + formula 的 draft 行),服务出的载荷:

### SERVED PAYLOAD ###
[{ "type":"object","name":"account","organizationId":null,
   "packageId":"app.hr","updatedAt":"t1","updatedBy":"ai" }]
### KEYS ### ['type','name','organizationId','packageId','updatedAt','updatedBy']
### leaks salary_grade? ### false
### leaks fields? ### false
### has .item? ### false

卡片的 repro jq '.[].item.fields.salary_grade' 恒为 undefined —— 因为 .item 本身不存在。全仓仅三处 listDrafts 定义(repo 投影 / protocol 包一层 { drafts } / SDK fetch 包装),投影只有一处,没有第二条能带 body 的路径。

卡片要求的动作与卡片的前提互相矛盾

#6599 要求「把这条出口加进共享 ADR-0106 用例表」。那张表装不下这条出口assertObjectSchemaMaskCasepackages/metadata-core/src/object-schema-fls-contract.ts)在非 fault 分支要求 served body 带 fields 记录,否则直接抛

the served body carries no fields record

_drafts 一个 fields 都没有。也就是说,如果当初有人照卡片说的去加那一行,门禁会立刻告诉他前提不对。表自己的形状在拒绝这条路由,而没人注意到这一点。

那么本 PR 交付什么

把「header-only 投影」这唯一屏障钉死

两条路由确实是 protocol.listDrafts() 的裸透传 —— 没有 applyObjectSchemaMask,没有能力门禁,requireAuth 之外什么都没有:

  • packages/rest/src/rest-server.ts —— res.json(result)
  • packages/runtime/src/domains/meta.ts —— deps.success(data)

它们今天安全,完全依赖这一个投影。所以任何人把 item 体加宽进 listDrafts(Studio 想要 diff 视图的字段级细节是最明显的拉力),两条路由会在那一瞬间开始向任何已认证调用方无掩码地服务对象 schema、requiredPermissions、picklist 选项值和 formula 商业 IP,而 ADR-0106 的 mask 覆盖不到这条路由。本用例在那一瞬间变红。

为什么这个屏障此前没被钉住

sys-metadata-repository-list-drafts.test.ts 原有 6 条用例的形状断言用的是 toMatchObject —— 非穷尽,对多出来的键免疫。它们断言的是名字、org 作用域和 WHERE 子句,没有一条能看见「载荷里多了一个 item 体」。这正是新用例必须用键集相等 + 全载荷字符串扫描(而非键白名单)来写的原因。

反向验证

预测(先写后跑):把 listDrafts 投影加宽为额外吐出 item: row.body(即本用例要防的那次「好心加宽」)后 —— 新用例变红,旧 6 条保持绿,因为 toMatchObject 对多出来的键免疫。若旧 6 条也红,则我对 toMatchObject 的判断是错的,照实报。

实测方向与预测一致:

 ❯ src/sys-metadata-repository-list-drafts.test.ts (7 tests | 1 failed)
   × projects headers ONLY — a draft row's stored body never reaches the caller (#6599)

AssertionError: expected [ 'item', 'name', …(5) ] to deeply equal [ 'name', 'organizationId', …(4) ]
+   "item",

 Tests  1 failed | 6 passed (7)

还原后 Tests 7 passed (7)

消费方普查(普查是先做的,它决定了 (a)/(b) 分叉怎么答)

在证伪之前先把「谁在调这两条路由」扫实了。结论:除 SDK 外,全部调用方都在创作面(Studio / Setup / AI 建站流),没有一个是面向终端用户的读面。

objectstack

  • packages/client/src/index.ts:826 —— SDK meta.listDrafts(),纯 fetch 包装。
  • CLI(packages/cli/src/):零命中
  • examples/零命中
  • cloud 仓可查(本容器未挂载)。

objectui —— 三簇

  1. Studio DesignStudioDesignSurface.tsx(4 处 client 调用 + 1 处裸 fetch)、PackageOwdOverviewPanel.tsxObjectHooksPanel.tsxpackageSurfaces.ts
  2. Metadata Admin / SetupPackagesPage.tsx(3 处裸 fetch)、inspectors/ObjectFieldInspector.tsx
  3. Preview / Home / Chatpreview/DraftPreviewBar.tsx(挂在 ConsoleLayout,仅 ?preview=draft 时拉取)、preview/draftStatus.tsAiChatPage 的 pending 计数)、preview/usePublishAllDrafts.tsconsole/home/HomePage.tsxPendingDraftsBanner

第 3 簇里唯一没有作者门禁的是 HomePagePendingDraftsBanner:它在 HomePage无条件渲染(两处渲染点都在 isAdmin ? … : … 分叉之外),所以非管理员的 Home 也会打这条路由。但它是一个创作期装置 —— 文档字符串写的是 AI 建站后的 publish 闭环,唯一动作是 Publish,失败处理是 .catch(() => { /* 不显示 */ })

这条普查现在服务的是「这条路由今天有谁在用」,而不是原本的 (a)/(b) 分叉。

残余(不在本 PR 处理)

去掉字段级泄露后,剩下的是存在性级别披露:已认证调用方仍能看到「app.hr 包里有一个叫 X 的 pending object draft,ai 在 t1 改过」。赌注比卡片描述的小一个量级,是否值得上门禁属产品判断,已交回 #6599 由维护者裁定,本 PR 不做。

测试

npx vitest run src/sys-metadata-repository-list-drafts.test.ts
 Test Files  1 passed (1)
      Tests  7 passed (7)

(原 6 条 + 新增 1 条。)

changeset

无。纯测试改动,不改任何运行时行为、不动任何线上载荷,发布不出东西 ⇒ 走 skip-changeset 标签。


Generated by Claude Code

os-project-manager and others added 2 commits August 9, 2026 08:40
#6599 认为 `GET /meta/_drafts` 会吐出 draft 对象的完整 `fields`。实测不成立:
`SysMetadataRepository.listDrafts` 是一个显式六键投影,从不读行上的 body。

两条路由(rest-server.ts / domains/meta.ts)确实是 `protocol.listDrafts()` 的
裸透传 —— 它们今天安全,完全依赖这一个投影。本用例把该依赖钉死:任何人把 item
体加宽进投影,两条路由会立刻开始无掩码地服务对象 schema,而 ADR-0106 的 mask
覆盖不到这条路由。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 9, 2026 9:10am

Request Review

@github-actions github-actions Bot added the size/s label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants